===== Participer à la conception d'une voiture autonome =====¶

Enjeux¶

Concevoir un premier modèle de segmentation d’images pour Future Vision Transport - une entreprise qui conçoit des systèmes embarqués de vision par ordinateur pour les véhicules autonomes. Le modèle de segmentation devra s’intégrer facilement dans la chaîne complète du système embarqué.

Objectifs¶

Techniquement parlant, notre responsabilité est de se charger de la segmentation des images. Plus précisément :

  1. Entraîner un modèle de segmentation des images
  2. Concevoir une API de prédiction et la déployer sur le Cloud
    • L’API prend en entrée une image et renvoie la segmentation de l’image de l’algo
  3. Concevoir une application web Flask de présentation des résultats et la déployer sur le Cloud
    • Cette application sera l’interface pour tester l’API et afficher les images et masks

L'objectif final est donc de créer une interface qui affichera les images et les masks.

Sommaire¶

Ce notebook a pour but un prétraitement de données.

  • Comprendre le jeu de données
    • Méthodologie
    • Masks : extraction d'images target
    • Images : alignement avec les masks
    • Choix de la métrique
  • Gestion des données
    • Gestion des catégories des images
    • Gestion de la volumétrie du corpus
      • Augmentation de données
      • Générateur de données
        • Test sans augmentation
        • Test avec augmentation
  • Sources
In [5]:
import glob
from distutils.dir_util import copy_tree
import os
import shutil
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import cv2
#from skimage import io, color
#from skimage.color import rgba2rgb, rgb2gray
import tqdm
import random
from random import shuffle
import time

import imgaug as ia
import imgaug.augmenters as iaa

import tensorflow as tf
from tensorflow.keras.preprocessing import image
from tensorflow.keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img
from tensorflow.keras import backend as K
from tensorflow.keras.optimizers import Adadelta, Nadam, Adam
from tensorflow.keras.models import Model, load_model, Sequential
from tensorflow.keras.utils import plot_model, Sequence, to_categorical
from tensorflow.keras.callbacks import TensorBoard, ModelCheckpoint, EarlyStopping, ReduceLROnPlateau, Callback
from tensorflow.keras.losses import binary_crossentropy
from tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, concatenate, Conv2DTranspose, UpSampling2D, BatchNormalization, Activation, Dropout, Reshape, Permute, Dense, GlobalAveragePooling2D

import segmentation_models as sm
from segmentation_models import Unet
from segmentation_models import get_preprocessing
from segmentation_models.losses import bce_jaccard_loss, DiceLoss, JaccardLoss, CategoricalCELoss
from segmentation_models.metrics import iou_score, FScore
2022-11-10 11:29:04.988003: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2022-11-10 11:29:05.702888: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/sylwia/anaconda3/lib/python3.9/site-packages/cv2/../../lib64:
2022-11-10 11:29:05.702954: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.
2022-11-10 11:29:05.794243: E tensorflow/stream_executor/cuda/cuda_blas.cc:2981] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2022-11-10 11:29:07.229478: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/sylwia/anaconda3/lib/python3.9/site-packages/cv2/../../lib64:
2022-11-10 11:29:07.229739: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/sylwia/anaconda3/lib/python3.9/site-packages/cv2/../../lib64:
2022-11-10 11:29:07.229753: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
Segmentation Models: using `keras` framework.
In [6]:
# Chemin vers les données
PATH = '/home/sylwia/Jupyter/P8_Segm_sem'
In [896]:
#pip show tensorflow
Name: tensorflow
Version: 2.10.0
Summary: TensorFlow is an open source machine learning framework for everyone.
Home-page: https://www.tensorflow.org/
Author: Google Inc.
Author-email: packages@tensorflow.org
License: Apache 2.0
Location: /home/sylwia/anaconda3/lib/python3.9/site-packages
Requires: absl-py, astunparse, flatbuffers, gast, google-pasta, grpcio, h5py, keras, keras-preprocessing, libclang, numpy, opt-einsum, packaging, protobuf, setuptools, six, tensorboard, tensorflow-estimator, tensorflow-io-gcs-filesystem, termcolor, typing-extensions, wrapt
Required-by: 
Note: you may need to restart the kernel to use updated packages.
In [897]:
pip list | grep Keras
Keras-Applications                1.0.8
Keras-Preprocessing               1.1.2
Note: you may need to restart the kernel to use updated packages.

Comprendre le jeu de données ¶

Le jeu de données à notre dispostion - Cityscapes - contient de diverses scènes de paysages urbains enregistrées à différents moments de l'année et venant de 50 villes différentes.

Le jeu de données contient 25 000 photos, pourvues de masks de segmentations (autrement dit annotations). 5000 de ces masks sont finement annotés, tandis que les 20 000 restantes sont beaucoup plus approximatifs. Pour notre projet, nous allons travailler avec les photographies disposant d'un mask de catégories finement annoté.

Les paires images - mask sont pré-divisées en train (2975 images), validation (500 images) et test (1525 images) sets.

Enfin, les photographies et les masks qui leur sont associés sont tous proposés en résolution 2048 x 1024.

Affichons les sets regroupant les villes respectives :

In [898]:
# Datasets
!ls ../P8_Segm_sem/data/
dataset  gtFine  leftImg8bit  models
In [899]:
# Images
print("Images pour l'entraînement :")
!ls ../P8_Segm_sem/data/leftImg8bit/train
print("\nImages pour validation :")
!ls ../P8_Segm_sem/data/leftImg8bit/val
print("\nImages pour tests :")
!ls ../P8_Segm_sem/data/leftImg8bit/test
Images pour l'entraînement :
aachen	cologne     erfurt   jena	      strasbourg  ulm
bochum	darmstadt   hamburg  krefeld	      stuttgart   weimar
bremen	dusseldorf  hanover  monchengladbach  tubingen	  zurich

Images pour validation :
frankfurt  lindau  munster

Images pour tests :
berlin	bielefeld  bonn  leverkusen  mainz  munich
In [900]:
# Masques
print("Masques pour l'entraînement :")
!ls ../P8_Segm_sem/data/gtFine/train
print("\nMasques pour validation :")
!ls ../P8_Segm_sem/data/gtFine/val
print("\nMasques pour tests :")
!ls ../P8_Segm_sem/data/gtFine/test
Masques pour l'entraînement :
aachen	cologne     erfurt   jena	      strasbourg  ulm
bochum	darmstadt   hamburg  krefeld	      stuttgart   weimar
bremen	dusseldorf  hanover  monchengladbach  tubingen	  zurich

Masques pour validation :
frankfurt  lindau  munster

Masques pour tests :
berlin	bielefeld  bonn  leverkusen  mainz  munich

Méthodologie ¶

La segmentation d'image sémantique est issue du domaine scientifique Computer vision. Il s'agit d'étiqueter des endroits spécifiques d'une image en fonction de ce qu'elle affiche : le modèle partitionne l'image en segments, chacun d'eux représentant une entité différente. Le résultat de la segmentation est donc une partition sémantique de l'image.

En effet, la segmentation sémantique consiste à étiqueter chaque pixel d’une image avec une classe correspondante à ce qui est représenté. On parle aussi de prédiction dense, car chaque pixel doit être prédit.

Le jeu de données fournit les images d'origine (ground truth) et leurs masks (images labellisées). Notre tâche consistera à en constituer un dataset que l'on pourra soumettre au modèle de machine learning. Pour cela, il faudra extraire les masks - (les images avec le suffixe labelIds) et les "coupler" les masks avec les images ground truth.

Masks : extraction d'images target ¶

Deux problèmes à affronter lors de l'extraction de masques :

  • Cibler la target. Il s'agit de récupérer uniquement les fichiers labellisés _gtFine_labelIds.
  • Uniformiser la nomenclature. Un masque doit avoir le même nom que l'image d'origine.

L'extraction se fera avec le module shutil, qui aide à automatiser les processus de manipulation de fichiers et de répertoires. La méthode shutil.copy2() sera utilisée pour copier le contenu du fichier source dans le fichier ou le répertoire de destination.

In [901]:
# Fonction pour identifier et extraire puis renommer les images-cibles
def extract_rename_file(directory, destination, sent_id) :

    for file in glob.iglob(f'{directory}/**/*'):
        if file.endswith(sent_id):
            filepath = os.path.join(directory, file)
            newfilepath = os.path.join(directory, file.replace(sent_id, ".png"))
            os.rename(filepath, newfilepath)
            shutil.copy2(newfilepath, destination)
In [902]:
# Path pour identifier des images-cibles
sent_id = "_gtFine_labelIds.png"

Extraction de masks depuis le dossier Train¶

In [903]:
# Fichier source
directory = PATH + '/data/gtFine/train/'
destination = PATH + r'/data/dataset/train_masks'

#extract_rename_file(directory, destination, sent_id)

Extraction de masks depuis le dossier Validation¶

In [904]:
# Fichier source
directory = PATH + '/data/gtFine/val/'
destination = PATH + r'/data/dataset/val_masks'

#extract_rename_file(directory, destination, sent_id)

Extraction de masks depuis le dossier Test¶

In [905]:
# Fichier source
directory = PATH + '/data/gtFine/test/'
destination = PATH + r'/data/dataset/test_masks'

#extract_rename_file(directory, destination, sent_id)

Images : alignement avec les masks ¶

A présent, il est nécessaire de renommer les fichiers pour qu'ils soient identiques pour les images et les masques.

In [906]:
# Images
!ls ../P8_Segm_sem/data/leftImg8bit
test  train  val

Train¶

In [907]:
directory = PATH + r'/data/leftImg8bit/train/'
destination = PATH + r'/data/dataset/train_img'
sent_id = "_leftImg8bit.png"

#extract_rename_file(directory, destination, sent_id)

Validation¶

In [908]:
directory = PATH + r'/data/leftImg8bit/val/'
destination = PATH + r'/data/dataset/val_img'
sent_id = "_leftImg8bit.png"

#extract_rename_file(directory, destination, sent_id)

Test¶

In [909]:
directory = PATH + r'/data/leftImg8bit/test/'
destination = PATH + r'/data/dataset/test_img'
sent_id = "_leftImg8bit.png"

#extract_rename_file(directory, destination, sent_id)
In [7]:
# C'EST PAR ICI

Vérification du dataset¶

Vérifions si nous avons bien le même nombre de masques et d'images dans les dossiers train, validation et test. Tout d'abord, créons les variables contenant les chemins d'accès :

In [8]:
!ls ../P8_Segm_sem/data/dataset/
dataset_compr.zip  test_masks  train_masks  val_masks
test_img	   train_img   val_img

Création de variables contenant le chemin d'accès vers les datasets respectifs :

In [9]:
train_images_dir = PATH +'/data/dataset/train_img'
train_masks_dir = PATH +'/data/dataset/train_masks'
val_images_dir = PATH +'/data/dataset/val_img'
val_masks_dir = PATH +'/data/dataset/val_masks'
test_images_dir = PATH +'/data/dataset/test_img'
test_masks_dir = PATH +'/data/dataset/test_masks'

Création de variables contenant l'ensemble d'images des datasets respectifs :

In [10]:
# Train set : images + masques
train_image_list = os.listdir(train_images_dir)
train_mask_list = os.listdir(train_masks_dir)
train_image_list.sort()
train_mask_list.sort()
In [11]:
train_mask_list[:3]
Out[11]:
['aachen_000000_000019.png',
 'aachen_000001_000019.png',
 'aachen_000002_000019.png']
In [12]:
# Validation set : images + masques
val_image_list = os.listdir(val_images_dir)
val_mask_list = os.listdir(val_masks_dir)
val_image_list.sort()
val_mask_list.sort()
In [13]:
# Trest set : images + masques
test_image_list = os.listdir(test_images_dir)
test_mask_list = os.listdir(test_masks_dir) 
test_image_list.sort()
test_mask_list.sort()

Affichons à présent le contenu des datasets - combien d'images et masques ? Sont-ils alignés ?

In [14]:
# Fonction qui permet de trier et afficher le volume + 10 premiers exemples
def check_volum(img_list, mask_list) :    
    # Affichage longueur
    print(f'Number of images: {len(img_list)}\nNumber of masks: {len(mask_list)}\n')
    
    # Affichage 10 premiers exemples
    for input_path, target_path in zip(img_list[:10], mask_list[:10]) :
        print(input_path, " | ", target_path)

Train

In [15]:
check_volum(train_image_list, train_mask_list)
Number of images: 2975
Number of masks: 2975

aachen_000000_000019.png  |  aachen_000000_000019.png
aachen_000001_000019.png  |  aachen_000001_000019.png
aachen_000002_000019.png  |  aachen_000002_000019.png
aachen_000003_000019.png  |  aachen_000003_000019.png
aachen_000004_000019.png  |  aachen_000004_000019.png
aachen_000005_000019.png  |  aachen_000005_000019.png
aachen_000006_000019.png  |  aachen_000006_000019.png
aachen_000007_000019.png  |  aachen_000007_000019.png
aachen_000008_000019.png  |  aachen_000008_000019.png
aachen_000009_000019.png  |  aachen_000009_000019.png

Validation

In [16]:
check_volum(val_image_list, val_mask_list)
Number of images: 500
Number of masks: 500

frankfurt_000000_000294.png  |  frankfurt_000000_000294.png
frankfurt_000000_000576.png  |  frankfurt_000000_000576.png
frankfurt_000000_001016.png  |  frankfurt_000000_001016.png
frankfurt_000000_001236.png  |  frankfurt_000000_001236.png
frankfurt_000000_001751.png  |  frankfurt_000000_001751.png
frankfurt_000000_002196.png  |  frankfurt_000000_002196.png
frankfurt_000000_002963.png  |  frankfurt_000000_002963.png
frankfurt_000000_003025.png  |  frankfurt_000000_003025.png
frankfurt_000000_003357.png  |  frankfurt_000000_003357.png
frankfurt_000000_003920.png  |  frankfurt_000000_003920.png

Test

In [17]:
check_volum(test_image_list, test_mask_list)
Number of images: 1525
Number of masks: 1525

berlin_000000_000019.png  |  berlin_000000_000019.png
berlin_000001_000019.png  |  berlin_000001_000019.png
berlin_000002_000019.png  |  berlin_000002_000019.png
berlin_000003_000019.png  |  berlin_000003_000019.png
berlin_000004_000019.png  |  berlin_000004_000019.png
berlin_000005_000019.png  |  berlin_000005_000019.png
berlin_000006_000019.png  |  berlin_000006_000019.png
berlin_000007_000019.png  |  berlin_000007_000019.png
berlin_000008_000019.png  |  berlin_000008_000019.png
berlin_000009_000019.png  |  berlin_000009_000019.png

Affichage du résultat¶

Les noms d'images et masques correspondent comme prévu. Affichons quelques photos et leurs masques :

In [18]:
def show_img_mask_idx(i, train_images_dir, train_masks_dir) :
    
    my_image = img_to_array(load_img(
        f'{train_images_dir}/{train_image_list[i]}'))/255.
    my_mask = img_to_array(load_img(
        f'{train_masks_dir}/{train_mask_list[i]}', color_mode="grayscale"))

    # Définition du nb de labels
    labels = np.unique(my_mask)
    print('\nCombien de labels sur cette image :', len(labels))

    my_mask = np.squeeze(my_mask)

    fig = plt.figure(figsize=(15, 15))
    ax = fig.add_subplot(1, 2, 1)
    ax.set_title('Image')
    ax.imshow(my_image)
    plt.axis("off")
    
    ax1 = fig.add_subplot(1, 2, 2)
    ax1.set_title('Mask')
    ax1.imshow(my_mask)
    plt.axis("off")
    plt.show()
In [19]:
i = 100

show_img_mask_idx(i, train_images_dir, train_masks_dir)
Combien de labels sur cette image : 11
In [20]:
i = 101

show_img_mask_idx(i, train_images_dir, train_masks_dir)
Combien de labels sur cette image : 16
In [21]:
i = 110

show_img_mask_idx(i, train_images_dir, train_masks_dir)
Combien de labels sur cette image : 18

Choix de la métrique ¶

Accuracy n'est pas une bonne métrique à utiliser dans une segmentation sémantique, car elle ne reflète pas le partitionnement des endroits d'une image. Il existe deux mesures permettant de mesurer cela :

  • Le coefficient de Dice (ou indice de Dice-Sørensen, ou score F1) Le coefficient se définit comme le double de l’intersection de deux lots (échantillons de valeurs) divisé par l’union de ces deux lots (cf la figure plus bas).
  • Le coefficient de Jaccard (IoU ou Intersection Over Union) L'IoU représente le ratio entre l'intersection du masque de segmentation réel et du masque prédit (vrais positifs), et l'union des 2 masques (vrais positifs + faux positifs + faux négatifs). Dans le cas de classes multiples, l'IoU de chaque classe est calculé et on prend leur moyenne.

Pour les deux mesures, on obtient un score entre 0 (pas d’intersection du tout) et 1 (masques identiques).

La métrique que nous avons surveillée de près est l’IoU. C'est la métrique de référence utilisée pour comparer les modèles de l'état de l'art. Nous avons noté également les scores F1 (Dice) tout au long de nos tests.

Gestion des données ¶

Les données nécessitent un prétraitement. Plusieurs points importants sont à soulever :

  • attribution de 8 catégories principales au lieu des 32 sous-catégories proposées
  • chargement de données et leur préparation au sein d'une classe Dataset (héritée de la classe de type Sequence)
  • mise en place d'un système de lecture de batchs au lieu de charger toutes les données (trop volumineuses)

Gestion des catégories d'images ¶

Le jeu de données propose des annotations qui couvrent 8 catégories et 32 sous-catégories. Il s'agit d'objets couramment rencontrées lors d'une conduite.

Notre objectif est de se focaliser sur les 8 catégories suivantes :

  • Vehicule
  • Human
  • Sky
  • Nature
  • Object (ex. panneaux de signalisation)
  • Construction
  • Flat (route)
  • Void (autres ou problèmes de labellisation)

Les « masks » de catégories étant prévus pour 30 catégories, il va falloir les convertir pour qu'ils représentent les 8 catégories principales. Chacune des 30 catégories sera donc affectée à l'une des 8 nouvelles catégories.

Essayons de les afficher :

In [22]:
mask_train_path = PATH + "/data/dataset/train_masks"
In [23]:
cats = {'void': [0, 1, 2, 3, 4, 5, 6],
        'flat': [7, 8, 9, 10],
        'construction': [11, 12, 13, 14, 15, 16],
        'object': [17, 18, 19, 20],
        'nature': [21, 22],
        'sky': [23],
        'human': [24, 25],
        'vehicle': [26, 27, 28, 29, 30, 31, 32, 33, -1]}
In [24]:
# Creation of mask from grey scale image
def create_mask(img, cats):
    '''creates an mask from image and segmentation categories

    Args:
      img - PIL image
      cats - dict {'cat1':[value1,value2,value3,etc...],'cat2':[value1,value2,value3,etc...]}

    Returns:
      A mask of type np.array of dimension (shape(img),len(cats)) '''


    img = tf.keras.preprocessing.image.img_to_array(img,dtype=np.int32) # convert img to np.array
    img = np.squeeze(img) #remove 1 dimension
    mask = np.zeros((img.shape[0], img.shape[1], len(cats)),dtype=int) # create a mask with zeros
    flat_cat = [val for cat in list(cats.values()) for val in cat] # create a list of all values associated with categories
    ca_min = min(flat_cat)
    ca_max = max(flat_cat)
    cats_names = list(cats.keys())

    #for each values associated with a category, fill in the mask with the corresponding category number
    for i in range(ca_min,ca_max):
            for idx,name in enumerate(cats_names):
                if i in cats[name]:
                    mask[:,:,idx] = np.logical_or(mask[:,:,idx],(img==i))
    #print('mask',mask)
    return mask
In [25]:
# Extraction de catégories via un tensor [:,:,i]
categories=[]

for file in train_mask_list :
    mask_file = mask_train_path +'/'+ file
    mask = tf.keras.preprocessing.image.load_img(mask_file,target_size=(128,256),color_mode="grayscale")
    mask_tensor = create_mask(mask,cats)
    cat=[]
    for i in range(8):
        cat.append(mask_tensor[:,:,i].sum())
    categories.append(cat)
In [26]:
# Conversion en array
categories=np.array(categories)
In [27]:
# Comptage sur l'axe 0
print(categories.sum(axis=0))
[10476029 37744963 21198078  1714486 14632973  3406053  1166331  6789915]

Affichons la répartition des 8 catégories principales :

In [28]:
# Affichage des catégories à l'aide d'un barplot
plt.figure(figsize=(10,6))
plt.bar(x=cats.keys(), 
        height=categories.sum(axis=0),
        tick_label=list(cats.keys())
       )
plt.xticks(rotation=45, fontsize=12)
plt.title('Répartition des classes\n', fontsize=14)
plt.tight_layout()

Les 8 catégories sont donc réparties de façon assez inégale. De quels types d'objets s'agit-il concrètement ? :

  • void - ground+, dynamic+, static+
  • flat - road, sidewalk, parking+, rail track+
  • construction - building, wall, fence, guard rail+, bridge+, tunnel+
  • object - pole, pole group+, traffic sign, traffic light
  • nature - vegetation, terrain
  • sky - sky
  • human - person, rider
  • vehicle - car, truck, bus, on rails, motorcycle, bicycle, caravan+, trailer+

Classe Dataset¶

Etant donné la nature de l’input, il faut trouver un moyen pour ne pas charger toutes les images pour l'entraînement. La solution est la classe Data Generator.

La classe Data Generator a permis de distribuer les images par batches (de taille paramétrable) et en parallèle appliquer les éventuels pré-processings (attribution des 8 classes au lieu des 32 sous-catégories) ou augmentation de données.

Une classe Dataset doit implémenter trois fonctions : init, len et getitem :

In [29]:
# The numpy.logical_or() method is used to c alculate truth values between x1 and x2 element-wise
In [30]:
# Classe for data loading and preprocessing
class CustomDataset:
    """Read images & preprocess images.
    
    Args:
        images_dir (str): path to images folder
        masks_dir (str): path to segmentation masks folder 
    
    """

    def __init__(
            self, 
            images_dir, 
            masks_dir 
    ):
        self.ids = os.listdir(images_dir)
        self.images_fps = [os.path.join(images_dir, image_id) for image_id in self.ids]
        self.masks_fps = [os.path.join(masks_dir, image_id) for image_id in self.ids]
        
    def __len__(self):
        return len(self.ids)
        
    def __getitem__(self, i):
        
        # read & preprocess data
        image = cv2.imread(self.images_fps[i])
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        image = image/255.0 # normalisation ! RGB images go from 0 to 255
        image = np.asarray(image).astype(np.float32)
        
        mask_ = cv2.imread(self.masks_fps[i], 0)
        mask_ = np.squeeze(mask_)
        mask = np.zeros((mask_.shape[0], mask_.shape[1], 8))
        
        for i in range(0, 34):
            if i in cats['void']:
                mask[:,:,0] = np.logical_or(mask[:,:,0],(mask_==i))
            elif i in cats['flat']:
                mask[:,:,1] = np.logical_or(mask[:,:,1],(mask_==i))
            elif i in cats['construction']:
                mask[:,:,2] = np.logical_or(mask[:,:,2],(mask_==i))
            elif i in cats['object']:
                mask[:,:,3] = np.logical_or(mask[:,:,3],(mask_==i))
            elif i in cats['nature']:
                mask[:,:,4] = np.logical_or(mask[:,:,4],(mask_==i))
            elif i in cats['sky']:
                mask[:,:,5] = np.logical_or(mask[:,:,5],(mask_==i))
            elif i in cats['human']:
                mask[:,:,6] = np.logical_or(mask[:,:,6],(mask_==i))
            elif i in cats['vehicle']:
                mask[:,:,7] = np.logical_or(mask[:,:,7],(mask_==i))        
        
        return image, mask
In [31]:
# Function pour visualiser les images
def visualize(**images):
    """PLot images in one row."""
    n = len(images)
    plt.figure(figsize=(20, 10))
    for i, (name, image) in enumerate(images.items()):
        plt.subplot(1, n, i + 1)
        plt.xticks([])
        plt.yticks([])
        plt.title(' '.join(name.split('_')).title())
        plt.imshow(image, cmap=plt.cm.bone)
    plt.show()

Regardons si cela fonctionne :

In [32]:
# Génération du dataset
tf_dataset = CustomDataset(train_images_dir, train_masks_dir)
In [33]:
type(tf_dataset)
Out[33]:
__main__.CustomDataset
In [34]:
# for image, mask in tf_dataset :
#     print(image[:1][0][0], "\n", mask[:1][0][0])
In [35]:
image, mask = tf_dataset[3]
In [36]:
print('Image shape:', image.shape)
print('Mask shape:', mask.shape)
Image shape: (1024, 2048, 3)
Mask shape: (1024, 2048, 8)
In [37]:
def visualize_img_mask(img, mask) :    
    plt.figure(figsize=(8, 6))
    plt.subplot(1, 1, 1)
    plt.title("Image")
    print(img.shape)
    plt.imshow(img)
    plt.axis('off')
    plt.show()
    
    plt.figure(figsize=(18, 12))
    plt.subplot(1, 2, 2)
    plt.title("Mask")
    mask_max = np.argmax(mask, axis=2)
    print(mask_max.shape)
    plt.imshow(mask_max)
    plt.axis('off')
    plt.show()
In [38]:
visualize_img_mask(image, mask)
(1024, 2048, 3)
(1024, 2048)
In [39]:
visualize(
    image=image,
#     Img_R=image[..., 0].squeeze(),
#     Img_G=image[..., 1].squeeze(),
#     Img_B=image[..., 2].squeeze(),
    void=mask[..., 0].squeeze(),
    flat=mask[..., 1].squeeze(),
    construction=mask[..., 2].squeeze(),
    obiect=mask[..., 3].squeeze(),
    nature=mask[..., 4].squeeze(),
    sky=mask[..., 5].squeeze(),
    human=mask[..., 6].squeeze(),
    vehicle=mask[..., 7].squeeze()
   
)

print('image_shape :',image.shape)
print('mask_shape :',mask.shape)
image_shape : (1024, 2048, 3)
mask_shape : (1024, 2048, 8)

Gestion de la volumétrie du corpus ¶

Il est important de tenir compte de l'instabilité du système d'acquisition d'images. Il s'agit d'images enregistrées à différents moments de l'année dans un paysage urbain, nous avons donc affaire à un grand flux de données entrant qui, de plus, est instable.

Pour remédier à ce problème, nous allons essayer de reproduire ces conditions en ayant recours d'une part à des techniques d'augmentation des données, et d'autre part à un générateur de données.

Augmentation de données ¶

Comment reproduire le comportement d'instabilité d'acquisition d'images ? Il faut pouvoir réaliser des transformations sur les images (ainsi que sur les masques de segmentation). L'objectif est de changer l'emplacement des pixels, c'est-à-dire créer des images artificielles en ayant recours à la Data Augmentation.

L’idée derrière la Data Augmentation est de reproduire les données préexistantes en leur appliquant une transformation aléatoire. Plusieurs avantages suite à cela :

  • augmentation de la taille du jeu de données
  • réduction de risques de sur-apprentissage

Lors de l’entraînement, notre modèle apprendra sur beaucoup plus de données tout en ne rencontrant jamais deux fois la même image.

Nous allons utiliser pour cela la librairie IMGAUG qui permet de nombreuses transformations.

Il existe plusieurs types de transformations :

  • Gaussian Blur
  • Random zoom
  • Random brightness
  • Horizontal flip

Gaussian blur

Un flou gaussien est le résultat du floutage d'une image par une fonction gaussienne.

In [40]:
def blur_img(img):

    n = random.uniform(0,2.0) # Generating random value for sigma
    blur_transform = iaa.Sequential([iaa.GaussianBlur(sigma=n)])
    img_blur = blur_transform(image=tf.keras.preprocessing.image.img_to_array(img))

    return tf.keras.preprocessing.image.array_to_img(img_blur)

Random zoom

Permet de zoomer de manière aléatoire à l’intérieur des images.

In [41]:
def zoom_img(img, mask):

    n = random.uniform(1, 5)  # Generating random value for sigma
    # uses order=0 to avoid artifacts in mask
    zoom_transform = iaa.Sequential([iaa.Affine(scale=n, order=0)])
    
    img_zoom = zoom_transform(
        image=tf.keras.preprocessing.image.img_to_array(img))
    mask_zoom = zoom_transform(
        image=tf.keras.preprocessing.image.img_to_array(mask))

    return tf.keras.preprocessing.image.array_to_img(img_zoom), tf.keras.preprocessing.image.array_to_img(mask_zoom, scale=False)

Random brightness

Permet de modifier la luminosité d'une image de manière aléatoire.

In [42]:
def brightness_img(img):

    br_transform = iaa.Sequential([iaa.MultiplyBrightness((4,2))])
    img_br = br_transform(image=tf.keras.preprocessing.image.img_to_array(img).astype(np.uint8))

    return tf.keras.preprocessing.image.array_to_img(img_br,scale=False)

Horizontal flip

Retourne horizontalement des images de manière aléatoire (autrement dit certaines seront retournées et d’autres non).

In [43]:
def flip_img(img,mask):

    flip_transform = iaa.Sequential([iaa.Fliplr()])
    img_flip = flip_transform(image=tf.keras.preprocessing.image.img_to_array(img))
    mask_flip = flip_transform(image=tf.keras.preprocessing.image.img_to_array(mask))

    return tf.keras.preprocessing.image.array_to_img(img_flip), tf.keras.preprocessing.image.array_to_img(mask_flip,scale=False)

Résultats

Affichons les images suite à la transformation aléatoire :

In [44]:
def show_image(item) :
    fig = plt.figure(figsize=(8, 12))
    plt.imshow(item)
    plt.axis("off")
In [45]:
# Annotations
!ls ../P8_Segm_sem/data/dataset/
dataset_compr.zip  test_masks  train_masks  val_masks
test_img	   train_img   val_img
In [88]:
# Images pour tests
test_img_file="data/dataset/val_img/munster_000116_000019.png"
test_mask_file="data/dataset/val_masks/munster_000116_000019.png"


# Loading images
img = tf.keras.preprocessing.image.load_img(test_img_file,target_size=(128,256))
mask = tf.keras.preprocessing.image.load_img(test_mask_file,target_size=(128,256),color_mode="grayscale")

Image de départ :

In [89]:
show_image(item=img)

Blur

In [90]:
show_image(item=blur_img(img))

Zoom

In [91]:
zooms = zoom_img(img,mask)
In [92]:
show_image(item=zooms[0])

Brightness

In [93]:
show_image(brightness_img(img))

Flip

In [94]:
flips = flip_img(img,mask)
In [95]:
show_image(flips[0])

Générateur de données ¶

Les générateurs sont des fonctions dans lequelles on stocke des variables, des éléments ou encore des images.

Les images étant volumineuses par essence, il est difficile de toutes les charger en mémoire pour entraîner nos modèles. J'ai donc mis en place un générateur de données adapté au jeu de données pour éviter ce problème.

Cette méthode est particulièrement pratique pour l’entraînement d’un modèle de Deep Learning qui fonctionne sur des lots de donnée(batch). Les lots sont chargés seulement lorsque le modèle en a besoin (par itération).

La classe CustomDataGenerator distribue donc les images par batch de taille paramétrable et en profite pour appliquer les éventuels pré-processing ou augmentations de données qui lui ont été transmis.

L’avantage des générateurs c’est qu’ils ne calculent pas la valeur de chaque élément. En effet, ils calculent les éléments uniquement lorsqu’on leur demande de le faire. C’est ce qu’on appelle une évaluation paresseuse (lazy evaluation). Elle permet d’utiliser immédiatement les données déjà calculées, pendant que le reste des données est en cours de calcul.

Les générateurs permettent donc un gain de rapidité et d’espace mémoire !

Les paramètres que l’on utilise :

  • le chemin du fichier, que l’on a déjà stocké dans des variables plus tôt
  • batch_size, la taille du lot d’images à charger. Ici on choisit des lots de 4 images.
  • target_size, le générateur peut redimensionner automatiquement la hauteur et la largeur des images chargés. On choisit une petite dimension (de 128×256) pour que le modèle s’entraîne plus rapidement

L'objectif est donc de constituer une liste composée du batch de données et de l'image. Nous obtenons en sortie un lot d'images de taille self.batch et et un tableau sous forme [image_batch, GT].

In [96]:
def normalize_input_img(img):
    '''Normalize PIL image to fall in [-1,1] range, returns 3D numpy array'''
    img =tf.keras.preprocessing.image.img_to_array(img,dtype=np.int32)
    img = img/255.
    img -= 1
    return img
In [97]:
class CustomDataGenerator(Sequence):

    def __init__(self, image_dir,
                 mask_dir,
                 batch_size,
                 img_height,
                 img_width,
                 cats,
                 sample_perc=100,
                 aug_blur=False,
                 aug_zoom=False,
                 aug_brightness=False,
                 aug_flip=False,
                 #augmentation=None,
                 preprocessing=None
                 ):

        self.image_dir = image_dir
        self.mask_dir = mask_dir
        self.image_filename = os.listdir(image_dir)
        self.image_filename.sort()
        self.mask_filename = os.listdir(mask_dir)
        self.mask_filename.sort()
        self.sample_perc = sample_perc

        # Generate a sample
        rdm_index = random.sample(range(0, len(self.image_filename)), int(
            len(self.image_filename)*self.sample_perc/100))
        image_filename_sample = []

        for i in rdm_index:
            image_filename_sample.append(self.image_filename[i])

        mask_filename_sample = []
        for i in rdm_index:
            mask_filename_sample.append(self.mask_filename[i])

        self.image_filename = image_filename_sample
        self.mask_filename = mask_filename_sample

        self.batch_size = batch_size
        self.img_height = img_height
        self.img_width = img_width
        self.cats = cats
        self.sample_perc = sample_perc
        self.aug_blur = aug_blur
        self.aug_zoom = aug_zoom
        self.aug_brightness = aug_brightness
        self.aug_flip = aug_flip
        #self.augmentation = augmentation
        self.preprocessing = preprocessing

    def __len__(self):
        return int(np.ceil(len(self.image_filename) / float(self.batch_size)))

    def __getitem__(self, idx):

        # generate random index for the batch
        idx = np.random.randint(0, len(self.image_filename)-1, self.batch_size)
        batch_img, batch_mask = [], []

        for i in idx:

            # filename
            img_file = self.image_dir+'/'+self.image_filename[i]
            mask_file = self.mask_dir+'/'+self.mask_filename[i]

            # Load as PIL and resize
            img = tf.keras.preprocessing.image.load_img(
                img_file, target_size=(self.img_height, self.img_width))
            mask = tf.keras.preprocessing.image.load_img(
                mask_file, target_size=(128, 256), color_mode="grayscale")

            # Normalize image and create mask from greyscale image
            img_norm = normalize_input_img(img)
            mask_tensor = create_mask(mask, self.cats)

            # Add to the batch
            batch_img.append(img_norm)
            batch_mask.append(mask_tensor)

            if self.aug_blur:
                batch_img.append(normalize_input_img(blur_img(img)))
                # When using blur augmentation,the mask is not changed
                batch_mask.append(mask_tensor)

            if self.aug_zoom:
                zooms = zoom_img(img, mask)
                batch_img.append(normalize_input_img(zooms[0]))
                batch_mask.append(create_mask(zooms[1], self.cats))

            if self.aug_brightness:
                batch_img.append(normalize_input_img(brightness_img(img)))
                # When using brightness augmentation,the mask is not changed
                batch_mask.append(mask_tensor)

            if self.aug_flip:
                flips = flip_img(img, mask)
                batch_img.append(normalize_input_img(flips[0]))
                batch_mask.append(create_mask(flips[1], self.cats))

            if self.preprocessing:
                batch_img = self.preprocessing(batch_img)

        return np.array(batch_img, dtype=float), np.array(batch_mask, dtype=float)

Test sans augmentation ¶

In [98]:
BATCH_SIZE=4
train_custom = CustomDataGenerator(train_images_dir, train_masks_dir, 
                                       BATCH_SIZE, 128, 256, cats,
                                       aug_blur=False, aug_zoom=False, aug_brightness=False, aug_flip=False)

Combien de batches ?

C'est la méthode __len__ qui calcule le nombre de lots que notre générateur est censé produire. Pour cela, nous divisons le nombre total d'échantillons par batch_size et ainsi nous obtenons la valeur finale de batches.

In [99]:
len(train_custom)
Out[99]:
744
In [100]:
batch_img, batch_mask = train_custom.__getitem__(100)
print(batch_img.shape, batch_mask.shape)
(4, 128, 256, 3) (4, 128, 256, 8)

On a bien des lots de 4 images de dimensions 128x256x3 et des lots de 4 masks à 8 couleurs (correspondant à nos catégories).

In [101]:
def visualize_batches(batch_x, batch_y) :
    fig = plt.figure(figsize=(15,15))
    for i, (img, mask) in enumerate(zip(batch_img,batch_mask)):
        ax = plt.subplot(BATCH_SIZE, 2, (i*2)+1)
        print(img.shape)
        plt.imshow((img*255).astype('uint8'))
        ax.axis('off')

        ax = plt.subplot(BATCH_SIZE, 2, (i*2)+2)
        mmask = np.argmax(mask, axis=2)
        plt.imshow(mmask)
        ax.axis('off')

    plt.tight_layout()
    plt.show()
In [102]:
visualize_batches(batch_img, batch_mask)
(128, 256, 3)
(128, 256, 3)
(128, 256, 3)
(128, 256, 3)

Test avec augmentation ¶

In [103]:
train_custom_augm = CustomDataGenerator(train_images_dir, train_masks_dir, 
                                        BATCH_SIZE, 128, 256, cats, 
                                        aug_blur=True, aug_zoom=True, aug_brightness=True, aug_flip=True)
In [104]:
len(train_custom_augm)
Out[104]:
744
In [105]:
batch_img_aug, batch_mask_aug = train_custom_augm.__getitem__(100)
print(batch_img_aug.shape, batch_mask_aug.shape)
(20, 128, 256, 3) (20, 128, 256, 8)
In [86]:
visualize_batches(batch_img_aug, batch_mask_aug)
(128, 256, 3)
(128, 256, 3)
(128, 256, 3)
(128, 256, 3)

Sources ¶

  • https://datascientest.com/u-net
  • https://inside-machinelearning.com/data-augmentation-ameliorer-rapidement-son-modele-de-deep-learning/
  • https://medium.datadriveninvestor.com/keras-training-on-large-datasets-3e9d9dbc09d4